Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
build:
# The type of runner that the job will run on
runs-on: ubuntu-22.04

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE
Expand All @@ -29,7 +29,7 @@ jobs:
- name: Setup go
uses: actions/setup-go@v4.1.0
with:
go-version: 1.19
go-version: 1.21.3

- name: Build
run: go build -v ./...
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v4.1.1.1.1
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
-
Expand Down
16 changes: 15 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
builds:
- ldflags:
- env:
- CGO_ENABLED=0
- GO111MODULE=on
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm
- arm64
goarm:
- "6"
- "7"
ldflags:
- -s -w -X main.BuildVersion={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.Date}}
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ At the current stage, supported backend are:
- [Google Drive](https://developers.google.com/drive) (see [doc](https://github.com/fclairamb/ftpserver/tree/master/fs/gdrive)) through [afero-gdrive](https://github.com/fclairamb/afero-gdrive)
- [SFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) through [afero's sftpfs](https://github.com/spf13/afero/)
- Email through [go-mail](https://github.com/go-mail/mail) thanks to [@x-way](https://github.com/x-way)
- Telegram through [telebot](https://github.com/tucnak/telebot) by [@slayer](https://github.com/slayer), see [doc](fs/telegram/README.md)

And with those are supported common parameters to switch them to read-only, enable login access, or use a temporary directory file (see [doc](https://github.com/fclairamb/ftpserver/tree/master/fs)).

Expand Down Expand Up @@ -191,6 +192,16 @@ Here is a sample config file:
"password": "password",
"hostname": "192.168.168.11:22"
}
},
{
"user": "telegram",
"pass": "telegram",
"fs": "telegram",
"shared": true,
"params": {
"Token": "<OBTAIN_TOKEN_FROM_BOTFATHER>",
"ChatID": "<INSERT_CHAT_ID_HERE>"
}
}
]
}
Expand Down
3 changes: 3 additions & 0 deletions fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/fclairamb/ftpserver/fs/mail"
"github.com/fclairamb/ftpserver/fs/s3"
"github.com/fclairamb/ftpserver/fs/sftp"
"github.com/fclairamb/ftpserver/fs/telegram"
)

// UnsupportedFsError is returned when the described file system is not supported
Expand Down Expand Up @@ -45,6 +46,8 @@ func LoadFs(access *confpar.Access, logger log.Logger) (afero.Fs, error) {
fs, err = gdrive.LoadFs(access, logger.With("component", "gdrive"))
case "dropbox":
fs, err = dropbox.LoadFs(access)
case "telegram":
fs, err = telegram.LoadFs(access, logger.With("component", "telegram"))
default:
fs, err = nil, &UnsupportedFsError{Type: access.Fs}
}
Expand Down
45 changes: 45 additions & 0 deletions fs/telegram/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# FTPServer Telegram connector

## Register bot

Read about telegram bots at https://core.telegram.org/bots/tutorial.

Bots are not allowed to contact users. You need to make the first contact from the user for which you want to set up the bot.

### Quick start

- Create a bot with [@BotFather](https://t.me/BotFather), let's say with username `my_ftp_bot`
- Get bot token from BotFather's response, use it as `Token` in config
- Get bot id by run `curl https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getMe`
- Find `@my_ftp_bot` in telegram and start chat with it
- Send `/start` to bot
- Run `curl https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates` and find your chat id in response, use it as `ChatID` in config


## Config example

Please note about `shared` flag. If it's `true` then bot instance will be shared between all connections.
If it's `false` then each user (or even each ftp connection) will have own bot instance and it can lead to telegram bot flood protection.

```json
{
"version": 1,
"accesses": [
{
"fs": "telegram",
"shared": true,
"user": "my_ftp_bot",
"pass": "my_secure_password",
"params": {
"Token": "<YOUR_BOT_TOKEN>",
"ChatID": "<YOUR_CHAT_ID>"
}

}
],
"passive_transfer_port_range": {
"start": 2122,
"end": 2130
}
}
```
20 changes: 20 additions & 0 deletions fs/telegram/example.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": 1,
"accesses": [
{
"fs": "telegram",
"shared": true,
"user": "test",
"pass": "test",
"params": {
"Token": "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789",
"ChatID": "123456789"
}

}
],
"passive_transfer_port_range": {
"start": 2122,
"end": 2130
}
}
79 changes: 79 additions & 0 deletions fs/telegram/fake_fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package telegram

import (
"os"
"sync"
)

// fakeFilesystem is a really simple and limited fake filesystem intended for store temporary info about files
// since some ftp clients expect to perform mkdir() + stat() on files and directories before upload
type fakeFilesystem struct {
sync.Mutex
dict map[string]*FileInfo
// dir fakeDir
}

type fakeDir struct {
name string
content []os.FileInfo
}

// newFakeFilesystem creates a new fake filesystem
func newFakeFilesystem() *fakeFilesystem {
return &fakeFilesystem{
dict: map[string]*FileInfo{},
// dir: fakeDir{content: []os.FileInfo{}},
}
}

// mkdir creates a directory
func (f *fakeFilesystem) mkdir(name string, mode os.FileMode) {
f.Lock()
defer f.Unlock()
f.dict[name] = &FileInfo{&FileData{
name: name,
dir: true,
mode: mode,
}}
}

// create creates a file
func (f *fakeFilesystem) create(name string) {
f.Lock()
defer f.Unlock()
f.dict[name] = &FileInfo{&FileData{
name: name,
dir: false,
}}
}

// setSize sets the size of a file
func (f *fakeFilesystem) setSize(name string, size int64) {
f.Lock()
defer f.Unlock()
if fileInfo, found := f.dict[name]; found {
fileInfo.size = size
}
}

// stat returns a file info
func (f *fakeFilesystem) stat(name string) *FileInfo {
f.Lock()
defer f.Unlock()
return f.dict[name]
}

// remove removes a file
func (f *fakeFilesystem) remove(name string) {
f.Lock()
defer f.Unlock()
delete(f.dict, name)
}

// exists checks if a file exists
func (f *fakeFilesystem) exists(name string) bool {
f.Lock()
defer f.Unlock()
_, ok := f.dict[name]
return ok
}
37 changes: 37 additions & 0 deletions fs/telegram/file_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package telegram

import (
"os"
"path/filepath"
"time"
)

// FileData is a simple structure to store file information and implement os.FileInfo interface
type FileData struct {
name string
dir bool
mode os.FileMode
modtime time.Time
size int64
}

type FileInfo struct {
*FileData
}

// Implements os.FileInfo
func (s *FileInfo) Name() string {
_, name := filepath.Split(s.name)
return name
}

func (s *FileInfo) Mode() os.FileMode { return s.mode }
func (s *FileInfo) ModTime() time.Time { return s.modtime }
func (s *FileInfo) IsDir() bool { return s.dir }
func (s *FileInfo) Sys() interface{} { return nil }
func (s *FileInfo) Size() int64 {
if s.IsDir() {
return int64(42)
}
return s.size
}
Loading