Skip to content

Commit d9fd27c

Browse files
committed
fix: Fix some issues with sharing files
1 parent 6e3f751 commit d9fd27c

File tree

9 files changed

+112
-29
lines changed

9 files changed

+112
-29
lines changed

agent/app/dto/request/file.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ type FileRemarkUpdate struct {
197197
}
198198

199199
type FileShareCreate struct {
200-
Path string `json:"path" validate:"required"`
201-
ExpireMinutes int `json:"expireMinutes" validate:"min=0,max=10080"`
202-
Password string `json:"password" validate:"omitempty,min=4,max=256"`
200+
Path string `json:"path" validate:"required"`
201+
ExpireMinutes int `json:"expireMinutes" validate:"min=0,max=10080"`
202+
Password *string `json:"password"`
203203
}

agent/app/dto/response/file.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ type FileShareInfo struct {
9393
ExpiresAt int64 `json:"expiresAt"`
9494
Permanent bool `json:"permanent"`
9595
HasPassword bool `json:"hasPassword"`
96+
Password string `json:"password,omitempty"`
9697
}
9798

9899
type FileSharePublicInfo struct {

agent/app/model/file_share.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type FileShare struct {
66
Token string `gorm:"not null;uniqueIndex" json:"token"`
77
FileName string `gorm:"not null" json:"fileName"`
88
ExpiresUnix int64 `json:"expiresUnix"`
9+
PasswordEnc string `json:"-"`
910
PasswordSalt string `json:"passwordSalt"`
1011
PasswordHash string `json:"passwordHash"`
1112
MaxDownloads int `json:"maxDownloads"`

agent/app/service/file_share.go

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/1Panel-dev/1Panel/agent/app/model"
2020
"github.com/1Panel-dev/1Panel/agent/app/repo"
2121
"github.com/1Panel-dev/1Panel/agent/buserr"
22+
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
2223
"gorm.io/gorm"
2324
)
2425

@@ -88,6 +89,17 @@ func shareModelToInfo(item model.FileShare) response.FileShareInfo {
8889
}
8990
}
9091

92+
func fillSharePassword(info *response.FileShareInfo, item model.FileShare) {
93+
if info == nil || item.PasswordEnc == "" {
94+
return
95+
}
96+
password, err := encrypt.StringDecrypt(item.PasswordEnc)
97+
if err != nil {
98+
return
99+
}
100+
info.Password = password
101+
}
102+
91103
func shareModelToPublicInfo(item model.FileShare) response.FileSharePublicInfo {
92104
return response.FileSharePublicInfo{
93105
FileName: item.FileName,
@@ -156,19 +168,29 @@ func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileSh
156168
item.ExpiresUnix = time.Now().Add(time.Duration(req.ExpireMinutes) * time.Minute).Unix()
157169
}
158170

159-
pw := strings.TrimSpace(req.Password)
160-
item.PasswordSalt = ""
161-
item.PasswordHash = ""
162-
if pw != "" {
163-
if utf8.RuneCountInString(pw) < 4 {
164-
return nil, buserr.New("ErrFileSharePasswordPolicy")
165-
}
166-
salt, err := randomSalt()
167-
if err != nil {
168-
return nil, err
171+
if req.Password != nil {
172+
pw := strings.TrimSpace(*req.Password)
173+
if pw == "" {
174+
item.PasswordEnc = ""
175+
item.PasswordSalt = ""
176+
item.PasswordHash = ""
177+
} else {
178+
pwLen := utf8.RuneCountInString(pw)
179+
if pwLen < 4 || pwLen > 256 {
180+
return nil, buserr.New("ErrFileSharePasswordPolicy")
181+
}
182+
enc, err := encrypt.StringEncrypt(pw)
183+
if err != nil {
184+
return nil, err
185+
}
186+
item.PasswordEnc = enc
187+
salt, err := randomSalt()
188+
if err != nil {
189+
return nil, err
190+
}
191+
item.PasswordSalt = salt
192+
item.PasswordHash = hashPassword(salt, pw)
169193
}
170-
item.PasswordSalt = salt
171-
item.PasswordHash = hashPassword(salt, pw)
172194
}
173195

174196
if isNew {
@@ -182,6 +204,7 @@ func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileSh
182204
}
183205

184206
res := shareModelToInfo(item)
207+
fillSharePassword(&res, item)
185208
return &res, nil
186209
}
187210

@@ -227,6 +250,7 @@ func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, erro
227250
return nil, nil
228251
}
229252
info := shareModelToInfo(item)
253+
fillSharePassword(&info, item)
230254
return &info, nil
231255
}
232256

agent/init/migration/migrations/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ var AddFileManageAISettings = &gormigrate.Migration{
12201220
}
12211221

12221222
var AddFileShareTable = &gormigrate.Migration{
1223-
ID: "20260407-add-file-share-table",
1223+
ID: "20260410-add-file-share-table",
12241224
Migrate: func(tx *gorm.DB) error {
12251225
return tx.AutoMigrate(&model.FileShare{})
12261226
},

core/constant/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ var DynamicRoutes = []string{
185185
`^/databases/mysql/setting/[^/]+/[^/]+$`,
186186
`^/databases/postgresql/setting/[^/]+/[^/]+$`,
187187
`^/websites/[^/]+/config/[^/]+$`,
188+
`^/s/[A-Za-z0-9]{10,16}$`,
188189
}
189190

190191
var CertStore atomic.Value

core/utils/security/security.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"github.com/gin-gonic/gin"
1919
)
2020

21+
var publicSharePagePattern = regexp.MustCompile(`^/s/[A-Za-z0-9]{10,16}$`)
22+
2123
func HandleNotRoute(c *gin.Context) bool {
2224
if !checkBindDomain(c) {
2325
HandleNotSecurity(c, "err_domain")
@@ -137,13 +139,21 @@ func checkFrontendPath(c *gin.Context) bool {
137139
if !isFrontendPath(c) {
138140
return false
139141
}
142+
if isPublicFileSharePagePath(c.Request.URL.Path) {
143+
return true
144+
}
140145
authService := service.NewIAuthService()
141146
if authService.GetSecurityEntrance() != "" {
142147
return authService.IsLogin(c)
143148
}
144149
return true
145150
}
146151

152+
func isPublicFileSharePagePath(path string) bool {
153+
reqUri := strings.TrimSuffix(path, "/")
154+
return publicSharePagePattern.MatchString(reqUri)
155+
}
156+
147157
func checkBindDomain(c *gin.Context) bool {
148158
settingRepo := repo.NewISettingRepo()
149159
status, _ := settingRepo.Get(repo.WithByKey("BindDomain"))

frontend/src/api/interface/file.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ export namespace File {
324324
expiresAt: number;
325325
permanent: boolean;
326326
hasPassword: boolean;
327+
password?: string;
327328
}
328329

329330
export interface FileSharePublicInfo {

frontend/src/views/host/file-management/share/index.vue

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
<el-option v-for="opt in expireOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
2424
</el-select>
2525
</el-form-item>
26-
<el-form-item :label="$t('file.sharePassword')" prop="password">
26+
<el-form-item :label="$t('file.sharePassword')" prop="sharePassword">
2727
<el-input
28-
v-model="form.password"
28+
v-model="form.sharePassword"
2929
type="password"
3030
show-password
3131
clearable
@@ -54,7 +54,7 @@
5454
</div>
5555
</el-form-item>
5656
<el-form-item :label="$t('file.shareExpiresAt')">
57-
<span>{{ expiresAtText }}</span>
57+
<el-input :model-value="expiresAtText" readonly />
5858
</el-form-item>
5959
</template>
6060
</el-form>
@@ -102,7 +102,7 @@ import i18n from '@/lang';
102102
import { GlobalStore } from '@/store';
103103
import { buildFileSharePageUrl, buildFileShareQrCodeUrl, copyText, dateFormat as formatDateTime } from '@/utils/util';
104104
import type { FormInstance, FormRules } from 'element-plus';
105-
import { computed, reactive, ref } from 'vue';
105+
import { computed, reactive, ref, watch } from 'vue';
106106
107107
interface ShareProps {
108108
path: string;
@@ -119,9 +119,12 @@ const expiresAtText = ref('');
119119
const qrCodeUrl = ref('');
120120
const qrDialogOpen = ref(false);
121121
const shareFormRef = ref<FormInstance>();
122+
const syncingPassword = ref(false);
123+
const initialSharePassword = ref('');
124+
const passwordTouched = ref(false);
122125
const form = reactive({
123126
expireMinutes: 1440,
124-
password: '',
127+
sharePassword: '',
125128
});
126129
127130
const emit = defineEmits(['close']);
@@ -149,9 +152,22 @@ const validatePassword = (_rule, value, callback) => {
149152
};
150153
151154
const rules = reactive<FormRules>({
152-
password: [{ validator: validatePassword, trigger: 'blur' }],
155+
sharePassword: [{ validator: validatePassword, trigger: 'blur' }],
153156
});
154157
158+
watch(
159+
() => form.sharePassword,
160+
(val) => {
161+
if (syncingPassword.value) {
162+
return;
163+
}
164+
if (val === initialSharePassword.value) {
165+
return;
166+
}
167+
passwordTouched.value = true;
168+
},
169+
);
170+
155171
const mapExpireMinutes = (info: File.FileShareInfo) => {
156172
if (info.permanent || info.expiresAt === 0) {
157173
return 0;
@@ -169,7 +185,11 @@ const applyShareInfo = (info: File.FileShareInfo | null) => {
169185
qrCodeUrl.value = '';
170186
qrDialogOpen.value = false;
171187
form.expireMinutes = 1440;
172-
form.password = '';
188+
form.sharePassword = '';
189+
syncingPassword.value = true;
190+
initialSharePassword.value = '';
191+
passwordTouched.value = false;
192+
syncingPassword.value = false;
173193
return;
174194
}
175195
shareUrl.value = buildFileSharePageUrl(info.code, globalStore.currentNode);
@@ -178,7 +198,11 @@ const applyShareInfo = (info: File.FileShareInfo | null) => {
178198
? i18n.global.t('website.ever')
179199
: formatDateTime(null, null, info.expiresAt * 1000);
180200
form.expireMinutes = mapExpireMinutes(info);
181-
form.password = '';
201+
syncingPassword.value = true;
202+
form.sharePassword = info.password || '';
203+
initialSharePassword.value = form.sharePassword;
204+
passwordTouched.value = false;
205+
syncingPassword.value = false;
182206
};
183207
184208
const loadShareDetail = async () => {
@@ -207,12 +231,21 @@ const generate = async () => {
207231
}
208232
loading.value = true;
209233
try {
210-
const pw = form.password.trim();
211-
const res = await createFileShare({
234+
const pw = form.sharePassword.trim();
235+
const payload: File.FileShareCreate = {
212236
path: filePath.value,
213237
expireMinutes: form.expireMinutes,
214-
...(pw.length > 0 ? { password: pw } : {}),
215-
});
238+
};
239+
240+
// Only send password when user explicitly changes it.
241+
if (passwordTouched.value) {
242+
payload.password = pw; // empty string means "clear password"
243+
} else if (!shareInfo.value?.hasPassword && pw.length > 0) {
244+
// New share: allow setting password when it wasn't loaded from server.
245+
payload.password = pw;
246+
}
247+
248+
const res = await createFileShare(payload);
216249
applyShareInfo(res.data as File.FileShareInfo);
217250
changed.value = true;
218251
} finally {
@@ -233,7 +266,19 @@ const cancelShare = async () => {
233266
234267
const copyLink = () => {
235268
if (shareUrl.value) {
236-
copyText(shareUrl.value);
269+
const password = form.sharePassword.trim();
270+
const content = password
271+
? `${i18n.global.t('file.shareLinkLabel')}:${shareUrl.value},${i18n.global.t(
272+
'file.sharePassword',
273+
)}:${password}`
274+
: `${i18n.global.t('file.shareLinkLabel')}:${shareUrl.value}`;
275+
copyText(content);
276+
}
277+
};
278+
279+
const openShareUrl = () => {
280+
if (shareUrl.value) {
281+
window.open(shareUrl.value, '_blank', 'noopener,noreferrer');
237282
}
238283
};
239284

0 commit comments

Comments
 (0)